<?php
namespace hid\storages\database;

use hconnection\base\Connection;
use hconnection\ConnectionManager;
use hid\base\IdBuilder;
use hid\base\IdStorage;
use Exception;
use PDO;
use PDOException;

/**
 * 数据库段号id存储器
 *<B>说明：</B>
 *<pre>
 * 略
 *</pre>
 */
class DatabaseStorage extends IdStorage
{
    /**
     * PDO 对象
     * @var PDO
     */
    protected $pdo;

    protected $dsn = '';

    protected $username = '';

    protected $password = '';

    /**
     * 表名
     * @var string
     */
    protected $table = '';

    /**
     * 是否启用连接管理
     * @var bool
     */
    protected $onConnectManager = false;
    
    /**
     * 连接类型
     * @var string
     */
    protected $connType = 'single';

    /**
     * @var Connection
     */
    protected $connection;

    public function __construct(array $config = [])
    {
        parent::__construct($config);

        $this->initConnectionManager($config);
    }

    /**
     * 获取pdo对象
     *<B>说明：</B>
     *<pre>
     *  单例
     *</pre>
     * @return PDO
     */
    protected function getPdo():PDO
    {
        if (is_null($this->pdo)) {
            $this->pdo = $this->createPdo();
        }

        return $this->pdo;
    }

    public function pingPdo(\PDO $pdo):bool
    {
        try {
            $pdo->query("SELECT 1"); // 轻量级查询
            return true; // 连接正常
        } catch (\PDOException $e) {
            return false; // 连接断开
        }
    }

    public function createPdo(array $config = []):PDO
    {
        if (!empty($config)) {
            $pdo = new \PDO($config['dsn'], $config['username'], $config['password']);
        } else {
            $pdo = new \PDO($this->dsn,  $this->username, $this->password);
        }

        return $pdo;
    }

    protected function initConnectionManager(array $config)
    {
        if ($this->onConnectManager === true) {
            $this->connection = ConnectionManager::createConnection($this->connType,$config);
            $this->connection->setCreator([$this,'createPdo'])
                ->setHeartbeat([$this,'pingPdo']);
        }
    }

    protected function exec(callable $callback,string $key)
    {
        if ($this->onConnectManager === true) {
            return $this->connection->exec($callback,$key);
        } else {
            return $callback($this->getPdo());
        }
    }

    /**
     * 执行查询 返回数据行
     * @param string $sql 执行sql
     * @param array $params sql 参数
     * @param string $key key
     * @return array
     * @throws Exception
     */
    protected function query(string $sql,array $params = [],string $key = ''):array
    {
        return $this->exec(function(PDO $pdo) use($sql,$params){
            $pdoStatement = $pdo->prepare($sql);
            foreach ($params as $key => $val) {
                $pdoStatement->bindValue(':' . $key, $val);
            }
            $result = $pdoStatement->execute();
            if (false === $result) {
                return [];
            } else {
                return $pdoStatement->fetchAll(PDO::FETCH_ASSOC);
            }
        },$key);
    }

    /**
     * 执行更新sql语句
     * @param string $sql 执行sql
     * @param array $params sql 参数
     * @param string $key key
     * @return int
     */
    protected function execute(string $sql,array $params = [],string $key = ''):int
    {
        return $this->exec(function(PDO $pdo) use($sql,$params){
            $pdoStatement = $pdo->prepare($sql);
            foreach ($params as $key => $val) {
                $pdoStatement->bindValue(':' . $key, $val);
            }

            //执行
            $result = $pdoStatement->execute();
            return $pdoStatement->rowCount();
        },$key);
    }

    protected function getKeyInfo(IdBuilder $idBuilder):array
    {
        $key =  $idBuilder->getStoreKey();
        $idInfo = $this->query('select id,max_id,version from ' . $this->table . '  where biz_type = :biz_type', ['biz_type' => $key],$key);
        if (!empty($idInfo)) {
            return $idInfo[0];
        } else {
            return [];
        }
    }

    protected function addKey(IdBuilder $idBuilder,int $initiValue = 1):int
    {
        $key = $idBuilder->getStoreKey();
        $sql = 'INSERT INTO ' . $this->table.  '(biz_type,max_id,init_value,version) VALUES (:biz_type,:max_id,:init_value,1)';
        $rownum = $this->execute($sql, ['biz_type' => $key,'max_id' => $initiValue,'init_value' => $idBuilder->getInitValue()],$key);
        $this->addInitValueCache($key);

        return $rownum;
    }

    public function incrBy(IdBuilder $idBuilder,int $step = 1):?int
    {
        $key = $idBuilder->getStoreKey();
        $idInfo = $this->getKeyInfo($idBuilder);
        if ($idBuilder->isInitValue() && !$this->hasInitValue($key) && empty($idInfo)) {
            $this->addKey($idBuilder,$idBuilder->getInitValueForStorage());
            $idInfo = $this->getKeyInfo($idBuilder);
        }

        $sql = 'update ' . $this->table . ' set max_id = max_id + :step,version = version + 1 where biz_type = :biz_type and version = :version';
        $rownum = $this->execute($sql, ['biz_type' =>$key,'step' => $step ,'version' => $idInfo['version']],$key);
        if ($rownum == 0) {
            return null;
        }

        $id = (int)$idInfo['max_id'] + $step;

        return $id;
    }

}
